//
//  Sphere.cpp
//  RayTracer
//
//  Created by 俞云康 on 3/25/16.
//  Copyright © 2016 yushroom. All rights reserved.
//

#include "Sphere.hpp"
#include "Params.hpp"


Sphere::Sphere(const std::string& name, const std::shared_ptr<Transform>& transform, float radius)
: Shape(name, transform), radius(radius), radius_squared(radius*radius) {
    center = transform->point2world(float3(0, 0 ,0));
    boundingBox.expandBy(Position(center.x+radius, center.y+radius, center.z+radius));
    boundingBox.expandBy(Position(center.x-radius, center.y-radius, center.z-radius));
}


shared_ptr<Shape> Sphere::create(const Params& params) {
    string name = params.getID();
    auto transform = Transform::create(params.child("transform"));
    float radius = params.getFloat("radius");
    return make_shared<Sphere>(name,transform, radius);
}


bool Sphere::intersect(const Ray& r, HitInfo* hit) const {
    float3 op = center - r.o;   // Solve t^2*d.d + 2*t*(o-p).d + (o-p).(o-p)-R^2 = 0
    float b = Dot(op, r.dir);   // a = dot(d, d) == 1, b = 2*dot(o-p, d), c = dot(o-p, o-p) - r*r
    float det = b*b - Dot(op, op) + radius_squared;
    if (det < 0)
        return false;
    det = sqrtf(det);
    float t = b - det > EPS ? b-det : b+det;
    if (t > EPS && t < r.maxt) {
        r.maxt = t;
        hit->position = r.o + r.dir * t;
        hit->normal = Normalize(hit->position - center);
        auto p = transform->point2local(hit->position); // local
        float phi = atan2f(p.y, p.x);
        float theta = acosf(p.z/radius);
        hit->texcoord.x = phi * INV_TWOPI;	// u
        hit->texcoord.y = theta * INV_PI;	// v
        //hit->dpdu = Normalize(Direction(-2*M_PI*pp.y, -2*M_PI*pp.x, 0));
        //hit->dpdv = Normalize(M_PI*Direction(pp.z*cosf(phi), pp.z*sinf(phi), -rad*sinf(theta)));
        float r_sin_theta = sqrtf(p.x*p.x + p.y*p.y);
        float cos_phi = p.x / r_sin_theta;
        float sin_phi = p.y / r_sin_theta;
        hit->dpdu = Normalize(transform->direction2world(Direction(-p.y, p.x, 0)));
        hit->dpdv = Normalize(transform->direction2world(Direction(p.z*cos_phi, p.z*sin_phi, -r_sin_theta)));
        //auto nn = transform.normal2world(Normalize(Cross(hit->dpdu, hit->dpdv)));
        //assert(nn == hit->normal);
        //TODO
        return true;
    }
    return false;
}

bool Sphere::intersect(const Ray& r) const {
    float3 op = center - r.o; // Solve t^2*d.d + 2*t*(o-p).d + (o-p).(o-p)-R^2 = 0
    float b = Dot(op, r.dir); // a = dot(d, d) == 1, b = 2*dot(o-p, d), c = dot(o-p, o-p) - r*r
    float det = b*b - Dot(op, op) + radius_squared;
    if (det < 0)
        return false;
    det = sqrtf(det);
    return ((b+det)>EPS) && ((b-det)<r.maxt);
}

bool Sphere::intersect_t(const Ray& r, float* t) const {
    float3 op = center - r.o; // Solve t^2*d.d + 2*t*(o-p).d + (o-p).(o-p)-R^2 = 0
    float b = Dot(op, r.dir); // a = dot(d, d) == 1, b = 2*dot(o-p, d), c = dot(o-p, o-p) - r*r
    float det = b*b - Dot(op, op) + radius_squared;
    if (det < 0)
        return false;
    det = sqrtf(det);
    *t = b - det > EPS ? b-det : b+det;
    return ((b+det)>EPS) && ((b-det)<r.maxt);
}


float Sphere::area() const {
    return 4*M_PI * radius_squared;
}

Position Sphere::sample(const Sample& sample, Normal* shape_normal) const {
    // p in local space
    Position p = radius * UniformSampleSphere(sample.x, sample.y);
    *shape_normal = Normalize(p);
    return transform->point2world(p);
}

Position Sphere::sample(const Position& p, const Sample& sample, Normal* shape_normal) const {
    // Compute coordinate system for sphere sampling
    Direction wc = Normalize(center - p);
    Direction wcX, wcY;
    CoordinateSystem(wc, &wcX, &wcY);
    
    float distance = DistanceSquared(p, center);
    // Sample uniformly on sphere if $\pt{}$ is inside it
    if (distance - radius_squared < 1e-4f)
        return this->sample(sample, shape_normal);
    // Sample sphere uniformly inside subtended cone
    float sinThetaMax2 = radius_squared / distance;
    float cosThetaMax = sqrtf(std::max(0.f, 1.f - sinThetaMax2));
    //DifferentialGeometry dgSphere;
    //HitInfo hit;
    Position ps;
    Ray r(p, UniformSampleCone(sample.x, sample.y, cosThetaMax, wcX, wcY, wc));
    float thit;
    if (!intersect_t(r, &thit))
        thit = Dot(center - p, Normalize(r.dir));
    //        else
    //            thit = r.maxt;
    ps = r.at(thit);
    *shape_normal = Normal(Normalize(ps - center));
    //if (ReverseOrientation) *ns *= -1.f;
    return ps;
}

float Sphere::pdf(const Position& p, const Direction& wi) const {
    // Return uniform weight if point inside sphere
    float dist2 = DistanceSquared(p, center);
    if (dist2 - radius_squared < 1e-4f) {
        return Shape::pdf(p, wi);
    }
    
    // Compute general sphere weight
    float sinThetaMax2 = radius_squared / dist2;
    float cosThetaMax = sqrtf(std::max(0.f, 1.f - sinThetaMax2));
    return UniformConePdf(cosThetaMax);
}